# kernel.rb
# Copyright (C) 2006-2009 Akira TAGOH

# Authors:
#   Akira TAGOH  <akira@tagoh.org>

# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.

# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.

# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330,
# Boston, MA 02111-1307, USA.

require 'rbconfig'
require 'thread'
require 'singleton'
require 'prune/debug'


=begin rdoc

== ObjectCache

=end

class ObjectCache
  include Singleton
  include PRUNE::Debug

  class << self
    include PRUNE::Debug

=begin rdoc

=== ObjectCache#enable

=end

    def enable
      @@is_enabled = true
    end # def enable

=begin rdoc

=== ObjectCache#disable

=end

    def disable
      @@is_enabled = false
    end # def disable

=begin rdoc

=== ObjectCache#is_enabled?

=end

    def is_enabled?
      @@is_enabled = true unless defined?(@@is_enabled)
      @@is_enabled
    end # def is_enabled?

=begin rdoc

=== ObjectCache#each_objects(&block)

=end

    def each_objects(&block)
      oc = ObjectCache.instance
      oc.cache.each(&block)
    end # def each_objects

=begin rdoc

=== ObjectCache#each_object(file, &block)

=end

    def each_object(file, &block)
      oc = ObjectCache.instance
      oc.cache[file].each(&block)
    end # def each_object

=begin rdoc

=== ObjectCache#get_klass(brief_name)

=end

    def get_klass(brief_name)
      oc = ObjectCache.instance
      return oc.klass2instance[brief_name.to_s]

      retval = nil
      begin
        ObjectCache.disable
        retval = eval(brief_name.to_s)
        raise NameError if retval.nil?
        # klass has already been registered without the anonymous module.
        # just go ahead with it to not get it confused.
      rescue NameError, TypeError
        # no registered name for klass.
        # try to look it up from the cache.
        oc = ObjectCache.instance
        retval = oc.klass2instance[brief_name.to_s]
      ensure
        ObjectCache.enable
      end

      retval
    end # def get_klass

=begin rdoc

=== ObjectCache#delete(file)

=end

    def delete(file)
      oc = ObjectCache.instance
      ObjectCache.each_object(file) do |name|
        oc.klass2instance.delete(name.to_s)
        oc.klasses.delete(name.to_s)
      end
      oc.cache.delete(file)
    end # def delete

=begin rdoc

=== ObjectCache#clear

=end

    def clear
      oc = ObjectCache.instance
      oc.cache.clear
      oc.klass2instance.clear
      oc.klasses.clear
    end # def clear

=begin rdoc

=== ObjectCache#cached?(file)

=end

    def cached?(file)
      oc = ObjectCache.instance
      oc.cache.has_key?(file)
    end # def cached?

=begin rdoc

=== ObjectCache#include?(klass)

=end

    def include?(klass)
      oc = ObjectCache.instance
      return oc.klasses.include?(klass.to_s)

      retval = nil
      begin
        ObjectCache.disable
        eval(klass.to_s)
        raise NameError if retval.nil?
        # klass has already been registered without the anonymous module.
        # just go ahead with it to not get it confused.
        retval = true
      rescue NameError, TypeError
        # no registered name for klass.
        # try to look it up from the cache.
        oc = ObjectCache.instance
        retval = oc.klasses.include?(klass.to_s)
      ensure
        ObjectCache.enable
      end

      retval
    end # def include?

=begin rdoc

=== ObjectCache#flush

=end

    def flush
      oc = ObjectCache.instance
      if oc.precache.length > 0 then
        new = objectlist
        file, orig = oc.precache.pop
        debug("kernel", "Flushing caches for %s...", file)
        merge_object(file, new, orig)
        oc.precache << [file, new]
      end
    end # def flush

=begin rdoc

=== ObjectCache#require(file)

=end

    def require(file)
      debug("kernel", "Loading %s[no cache]...", file)
      oc = ObjectCache.instance
      ObjectCache.flush
      retval = ::Kernel.require(file)
      new = objectlist
      if oc.precache.length > 0 then
        # update to exclude Module/Class being loaded in this file.
        oc.precache[-1][1] = new
      end

      retval
    end # def require

=begin rdoc

=== ObjectCache#load(file)

=end

    def load(file)
      debug("kernel", "Loading %s...", file)
      oc = ObjectCache.instance
      ObjectCache.flush
      orig = objectlist
      loading_files = oc.precache.map {|f,l| f}
      if loading_files.include?(file) then
        false
      else
        oc.precache << [file, orig]
        begin
          ::Kernel.load(file, true)
        ensure
          file, orig = oc.precache.pop
        end
        new = objectlist
        merge_object(file, new, orig)
        if oc.precache.length > 0 then
          # update to exclude Module/Class being loaded in this file.
          oc.precache[-1][1] = new
        end

        true
      end
    end # def load

    private

    def objectlist
      list = []
      ObjectSpace.each_object(Class) do |klass|
        list.push(klass)
      end
      ObjectSpace.each_object(Module) do |mod|
        list.push(mod)
      end

      return list
    end # def objectlist

    def merge_object(file, new, orig)
      oc = ObjectCache.instance
      (new.uniq - orig.uniq).each do |klass|
        brief_name = klass.to_s.sub(/\A#<Module:0x[0-9a-f]+>::/, '')
        next if brief_name =~ /\APRUNE/
        debug('kernel', "Merging %s in %s...", brief_name, file)
        if ObjectCache.include?(brief_name) then
          oklass = ObjectCache.get_klass(brief_name)
          klass.__send__(:class_variable_set, "@@destination_of_mm", oklass)
          klass.module_eval <<-EOS, __FILE__, __LINE__+1
            def #{klass}.method_missing(sym, *a)
              @@destination_of_mm.method_missing(sym, *a)
            end
            def #{klass}.const_missing(sym, *a)
              @@destination_of_mm.const_missing(sym, *a)
            end
          EOS
          klass.instance_variables.each do |x|
            debug('kernel', "Merging the instance variable %s on %s...", x, brief_name)
            ov = klass.instance_variable_get(x)
            oklass.instance_variable_set(x, ov)
          end
          klass.__send__(:class_variables).each do |x|
            debug('kernel', "Merging the class variable %s on %s...", x, brief_name)
            ov = klass.__send__(:class_variable_get, x)
            oklass.__send__(:class_variable_set, x, ov)
          end
          (klass.constants - oklass.constants).each do |x|
            debug('kernel', "Merging the constant %s on %s...", x, brief_name)
            ov = klass.const_get(x)
            oklass.const_set(x, ov)
          end
          (klass.included_modules - oklass.included_modules).each do |m|
            debug('kernel', "Merging the included module %s on %s...", m, brief_name)
            oklass.__send__(:include, m)
          end
          (klass.instance_methods(false) - oklass.instance_methods(false)).each do |m|
            debug('kernel', "Merging the public method %s on %s...", m, brief_name)
            em = m.unpack('H*')[0]
            oklass.__send__(:class_variable_set, "@@destination_of_#{em}",
                            klass.instance_methods.include?(m) ? klass.instance_method(m) : klass.method(m))
            oklass.__send__(:class_variable_set, "@@origin_of_#{em}",
                            klass.instance_methods.include?(m) ? klass : nil)
            oklass.module_eval <<-EOS, __FILE__,__LINE__+1
              def #{m}(*args)
                origin = @@origin_of_#{em}
                if origin.nil? then
                  @@destination_of_#{em}.bind(origin).call(*args)
                else
                  @@destination_of_#{em}.call(*args)
                end
              end
              public :#{m}
            EOS
          end
          (klass.private_methods(false) - oklass.private_methods(false)).each do |m|
            debug('kernel', "Merging the private method %s on %s...", m, brief_name)
            em = m.unpack('H*')[0]
            oklass.__send__(:class_variable_set, "@@destination_of_#{em}",
                            klass.instance_methods.include?(m) ? klass.instance_method(m) : klass.method(m))
            oklass.__send__(:class_variable_set, "@@origin_of_#{em}",
                            klass.instance_methods.include?(m) ? klass : nil)
            oklass.module_eval <<-EOS, __FILE__,__LINE__+1
              def #{m}(*args)
                origin = @@origin_of_#{em}
                if origin.nil? then
                  @@destination_of_#{em}.bind(origin).call(*args)
                else
                  @@destination_of_#{em}.call(*args)
                end
              end
              private :#{m}
            EOS
          end
          (klass.protected_methods(false) - oklass.protected_methods(false)).each do |m|
            debug('kernel', "Merging the protected method %s on %s...", m, brief_name)
            em = m.unpack('H*')[0]
            oklass.__send__(:class_variable_set, "@@destination_of_#{em}",
                            klass.instance_methods.include?(m) ? klass.instance_method(m) : klass.method(m))
            oklass.__send__(:class_variable_set, "@@origin_of_#{em}",
                            klass.instance_methods.include?(m) ? klass : nil)
            oklass.module_eval <<-EOS, __FILE__,__LINE__+1
              def #{m}(*args)
                origin = @@origin_of_#{em}
                if origin.nil? then
                  @@destination_of_#{em}.bind(origin).call(*args)
                else
                  @@destination_of_#{em}.call(*args)
                end
              end
              protected :#{m}
            EOS
          end
          (klass.singleton_methods(false) - oklass.singleton_methods(false)).each do |m|
            debug('kernel', "Merging the singleton method %s on %s...", m, brief_name)
            em = m.unpack('H*')[0]
            oklass.__send__(:class_variable_set, "@@destination_of_#{em}",
                            klass.instance_methods.include?(m) ? klass.instance_method(m) : klass.method(m))
            oklass.__send__(:class_variable_set, "@@origin_of_#{em}",
                            klass.instance_methods.include?(m) ? klass : nil)
            oklass.module_eval <<-EOS, __FILE__,__LINE__+1
              def #{brief_name}.#{m}(*args)
                origin = @@origin_of_#{em}
                if origin.nil? then
                  @@destination_of_#{em}.bind(origin).call(*args)
                else
                  @@destination_of_#{em}.call(*args)
                end
              end
            EOS
          end
        else
          begin
            ObjectCache.disable
            mod = eval(brief_name)
            klass.__send__(:class_variable_set, "@@destination_of_mm", mod)
            klass.module_eval <<-EOS, __FILE__, __LINE__+1
              def #{klass}.method_missing(sym, *a)
                @@destination_of_mm.method_missing(sym, *a)
              end
              def #{klass}.const_missing(sym, *a)
                @@destination_of_mm.const_missing(sym, *a)
              end
            EOS
          rescue NameError, TypeError
            # ignore.
          ensure
            ObjectCache.enable
          end
          debug('kernel', "cached %s in %s", brief_name, file)
          # !ObjectCache.include?(brief_name)
          oc.klass2instance[brief_name] = klass
          oc.klasses << brief_name
          oc.cache[file] = [] unless oc.cache.has_key?(file)
          oc.cache[file] << brief_name
        end
      end # (new.uniq - orig.uniq).each
    end # def merge_object

  end

=begin rdoc

=== ObjectCache#instance

=end

  def initialize
    @precache = []
    @cache = {}
    @cache.default = []
    @klasses = []
    @klass2instance = {}
  end # def initialize

  attr_reader :precache, :cache, :klasses, :klass2instance

end # class ObjectCache

class Object

=begin
  def require(file, *args)
    retval = false

    Thread.exclusive do
      begin
        retval = load("#{file}.rb", *args)
      rescue LoadError => e
        if e.to_s =~ /#{file}.rb\Z/ || e.to_s =~ /#{file}\Z/ then
          begin
            # the module that now is attempting to load may be a dynamic loadable module.
            # So giving up to manage to load as the anonymous module.
            ObjectCache.require(file)
          rescue LoadError
            raise LoadError.new("no such file to load -- #{file}")
          end
        else
          raise
        end
      end
    end

    return retval
  end # def require
=end

  def load(file, *args)
    retval = true

    Thread.exclusive do
      if ObjectCache.cached?(file) then
        return false
      end

      retval = ObjectCache.load(file)
    end

    return retval
  end # def load

end # class Object

class Module

  alias :original_const_missing :const_missing

  def const_missing(klass_id)
    if ObjectCache.is_enabled? then
      ObjectCache.flush
      brief_name = self == Object ? '' : "#{self.to_s.sub(/\A#<Module:0x[0-9a-f]+>::/, '')}"
      if ObjectCache.include?("#{brief_name}::#{klass_id}") then
        return ObjectCache.get_klass("#{brief_name}::#{klass_id}")
      end
      oc = ObjectCache.instance
      oc.klass2instance.each do |k, v|
        if k == brief_name && v.constants.include?(klass_id.to_s.sub(/\A::/,'')) then #
          return v
        end
      end
      if ObjectCache.include?(klass_id) then
        return ObjectCache.get_klass(klass_id)
      end
    end

    raise NameError.new("uninitialized constant #{brief_name}#{brief_name.empty? ? '' : '::'}#{klass_id}")
  end # def const_missing

  alias :original_instance_method :instance_method

  def instance_method(symbol)
    retval = nil
    begin
      retval = original_instance_method(symbol)
    rescue NameError => e
      if self.to_s =~ /#<Module:0x[0-9a-f]+>::/ then #
        ObjectCache.disable
        begin
          retval = eval(self.to_s.sub(/#<Module:0x[0-9a-f]+>::/, '')) #
          retval.instance_method(symbol)
        rescue NameError => e
          raise e
        ensure
          ObjectCache.enable
        end
      end
    end

    retval
  end # def instance_method

end # class Module

class Class

  def const_missing(klass_id)
    if [Object, Kernel].include?(self) then
      super
    else
      begin
        Object.send(:const_missing, klass_id)
      rescue NameError
        name = self.to_s.sub(/\A#<Module:0x[0-9a-f]+>::/, '')
        klass = nil
        begin
          klass = eval(name)
        rescue NameError
          raise NameError.new("uninitialized constant #{klass_id}")
        end

        unless klass.constants.include?(klass_id.to_s) then
          # try to look up on the parent class
          if klass.to_s.include?('::') then
            name = klass.to_s.sub(/\A(.*)::[^:]+\Z/, '\1')
            name.sub!(/\A#<Module:0x[0-9a-f]+>::/, '')
            begin
              klass = eval(name)
            rescue NameError
              raise NameError.new("uninitialized constant #{klass_id}")
            end
          end
        end

        unless klass.constants.include?(klass_id.to_s) then
          raise NameError.new("uninitialized constant #{klass_id} in #{klass}")
        end

        return klass.const_get(klass_id)
      end
    end
  end # def const_missing

end # class Class
